package xfile

import (
	"bufio"
	"crypto/md5"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"
	"time"

	"gitee.com/xfrm/middleware/xcontainer"
	"gitee.com/xfrm/middleware/xutil"
)

const IO_BUFFER_SIZE = 4096

var (
	ErrEmptyArguments = errors.New("Argument Cann't be empty")
)

/*
判断文件是否存在，该函数和PHP的file_exists一致，当filename 为 文件/文件夹/符号链接 时均返回 true
*/
func FileExists(filename string) bool {
	_, err := os.Stat(filename)
	if err != nil {
		if os.IsNotExist(err) {
			return false
		}
	}
	return true
}

/*
判断给定的filename是否是一个文件
*/
func IsFile(filename string) (bool, error) {

	if len(filename) <= 0 {
		return false, ErrEmptyArguments
	}

	stat, err := os.Stat(filename)
	if err != nil {
		return false, err
	}

	if stat.IsDir() {
		return false, nil
	}

	return true, nil
}

// FileSize 获取文件大小
func FileSize(filename string) (int64, error) {
	if len(filename) <= 0 {
		return 0, ErrEmptyArguments
	}

	stat, err := os.Stat(filename)
	if err != nil {
		return 0, err
	}

	if !stat.IsDir() {
		return stat.Size(), nil
	}
	return 0, os.ErrInvalid
}

// FileCopy 将文件从src Copy 到 dest
func FileCopy(src string, dest string) error {
	sfp, err := os.Open(src)
	if err != nil {
		return err
	}
	defer sfp.Close()

	ofp, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	_, err = io.Copy(ofp, sfp)
	if err != nil {
		ofp.Close()
		return err
	}
	return ofp.Close()
}

// DirExists check whether a directory exists.
func DirExists(p string) bool {
	if fi, err := os.Stat(p); err == nil {
		return fi.IsDir()
	}
	return false
}

// DirEmpty check whether a directory empty.
func DirEmpty(p string) bool {
	fs, err := ioutil.ReadDir(p)
	if err != nil {
		return false
	}
	return len(fs) == 0
}

// DirSize 递归获取一个文件夹下所有文件的大小
func DirSize(filename string) (int64, error) {
	if len(filename) <= 0 {
		return 0, ErrEmptyArguments
	}
	var (
		size = int64(0)
	)
	err := EachFile(filename, func(path string, fio os.FileInfo) error {
		if !fio.IsDir() {
			fsz, e := FileSize(path)
			if e != nil {
				return e
			}
			size += fsz
		}
		return nil
	})
	if err != nil {
		return 0, err
	}
	return size, nil
}

/*
判断给定的filename是否是一个目录
*/
func IsDir(filename string) (bool, error) {

	if len(filename) <= 0 {
		return false, ErrEmptyArguments
	}

	stat, err := os.Stat(filename)
	if err != nil {
		return false, err
	}

	if !stat.IsDir() {
		return false, nil
	}

	return true, nil
}

/*
获取文件的最后修改时间
*/
func GetFileModifyTime(filename string) (time.Time, error) {

	fileInfo, err := os.Stat(filename)
	if err != nil {
		return time.Time{}, err
	}

	return fileInfo.ModTime(), nil
}

/**
给路径path 之后加上 name，该函数主要处理 path 尾部的 / 和 name 最前面的 / 的问题
*/
func PathAppendName(path, name string) string {
	if path[len(path)-1] == '/' {
		if name[0] == '/' {
			return path + name[1:]
		}
		return path + name
	}
	if name[0] == '/' {
		return path + name
	}
	return path + "/" + name
}

func SearchFileInPath(filename string, paths ...string) (fullpath string, err error) {
	for _, path := range paths {
		if strings.HasSuffix(path, "/") {
			fullpath = path + filename
		} else {
			fullpath = path + "/" + filename
		}
		if FileExists(fullpath) {
			return
		}
	}
	fullpath = ``
	err = os.ErrNotExist
	return
}

/**
计算一个文件的行数
*/
func LineCount(pathname string) (cnt uint32, err error) {
	var fp *os.File
	fp, err = os.Open(pathname)
	if err != nil {
		return
	}
	defer fp.Close()

	var buf []byte = make([]byte, 1024)
	var rdSize int
	for {
		rdSize, err = fp.Read(buf)
		if rdSize > 0 {
			for i := 0; i < rdSize; i++ {
				if buf[i] == '\n' {
					cnt++
				}
			}
		} else {
			if err == io.EOF {
				err = nil
				break
			} else {
				return 0, err
			}
		}
	}
	return
}

//行回调函数，当该函数返回 false 时，回调终止，不再继续
type LineCallback func(line string) (doContinue bool)

//对文件中每一行内容进行回调,当 lc 返回 false 时回调也会终止
func EachLine(pathname string, lc LineCallback, skipEmpty bool) (err error) {
	var fp *os.File
	fp, err = os.Open(pathname)
	if err != nil {
		return
	}
	defer fp.Close()

	rd := bufio.NewReader(fp)
	for {
		line, err := rd.ReadString('\n')
		if err == nil {
			line = strings.TrimRightFunc(line, func(r rune) bool {
				if r == '\r' || r == '\n' {
					return true
				}
				return false
			})
			if skipEmpty {
				if line == `` {
					continue
				}
			}
			if !lc(line) {
				break
			}
		} else {
			if err == io.EOF {
				err = nil
				break
			} else {
				return err
			}
		}
	}
	return
}

type FileInfoCallback func(pathname string, fi os.FileInfo) error

/*
递归的遍历某个文件/文件夹中的所有对象，
*/
func EachFile(filepath string, fic FileInfoCallback) error {
	if filepath == `` {
		return ErrEmptyArguments
	}
	fi, err := os.Stat(filepath)
	if err != nil {
		return err
	}
	if !fi.IsDir() {
		return errors.New("filepath is not dir")
	}
	var stack xcontainer.Stack
	stack.Push(filepath)
	for {
		if stack.Len() <= 0 {
			break
		}
		se := stack.Pop()
		if se == nil {
			break
		}
		if fpath, ok := se.Value.(string); ok {
			fp, err := os.Open(fpath)
			if err != nil {
				fmt.Printf("try to iterate path %s but failed, error is: %#v\n", fpath, err)
				continue
			}
			dirs, err := fp.Readdir(-1)
			if err != nil {
				fmt.Printf("try to read %s sub items but failed, error is: %#v\n", fpath, err)
				fp.Close()
				continue
			}
			fp.Close()
			for _, sfi := range dirs {
				pathname := PathAppendName(fpath, sfi.Name())
				if sfi.IsDir() {
					stack.Push(pathname)
				}
				err := fic(pathname, sfi)
				if err != nil {
					return err
				}
			}
		}
	}
	return nil
}

func LoadFile(filePath string) (string, error) {
	b, err := ioutil.ReadFile(filePath)
	if err != nil {
		return "", err
	}
	return xutil.Bytes2Str(b), nil
}

func MD5Sum(filePath string) ([]byte, error) {
	in, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
	defer in.Close()

	m5h := md5.New()
	_, err = io.Copy(m5h, in)
	if err != nil {
		return nil, err
	}
	return m5h.Sum([]byte(``)), nil
}

func PutFile(filePath string, contents []byte, append bool) error {
	var (
		out *os.File
		err error
	)
	if append {
		out, err = os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 066)
	} else {
		out, err = os.Create(filePath)
	}
	if err != nil {
		return err
	}
	defer out.Close()
	return ensureWrite(out, contents)
}

func ensureWrite(out io.Writer, bytes []byte) error {
	var (
		total      = len(bytes)
		writed int = 0
	)
	for {
		if writed >= total {
			return nil
		}
		n, err := out.Write(bytes[writed:])
		if err != nil {
			return err
		}
		if n <= 0 {
			return fmt.Errorf("Cann't write bytes to io.Writer, it writes %d bytes", n)
		}
		writed += n
	}
	return nil
}
